/**
 * Name: Ofri Keidar
 * ID: 302933981
 * Group: 04
 * Exercise: ex41
 * Linux Distribution:Ubuntu
 * Version: 12.04.2 LTS, Precise Pangolin
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <sys/sem.h>
#include <sys/ipc.h>
#include <pthread.h>

#define ARGS_NUM 2 // number of needed arguments to main function

// server properties
#define SOCKET_PROTOCOL 0 // protocol used for socket's family
#define MAX_CLIENTS 5 // maximal number of clients that can connect at the same time

// message details
#define MSG_MAX_SIZE 160*sizeof(char) // maximal message size
#define GET_NICKNAME_MSG "Please enter your nick name" // message to a new client asking for a nickname
#define NICKNAME_TAKEN_MSG "User exists, please enter a different name" // message to a new client asking for another nickname
#define NEW_USER_CONNECTED_MSG "The user %s joined the chat" // message to all connected clients, notifying that a new user has connected
#define DECLARE_REGISTERED_CLIENTS_MSG "The following users are connected to the chat:" // message declaring currently registered clients
#define NO_RECIPIENT_MSG "User does not exist" // message to client indicating no user matching recipient in private message
#define SELF_RECIPIENT_MSG "You cannot send a message to yourself" // message to client trying to send message to him/herself
#define ERROR_IN_FORMAT_MSG "Please write message in format of \"All:...\" or \"To:Nickname:...\"" // message to client asking for a message in correct format
#define CLOSE_CONNECTION_MSG "Close" // message from client to indicate to close connection
#define USER_DISCONNECTED_MSG "" // message to all connected clients, notifying that a user has disconnected
#define REJECT_MSG "Sorry, but there are already %d users in chat. Please try again later" // message to client attempting to connect when there are connected MAX_CLIENTS clients

// return values of boolean functions
#define TRUE 1
#define FALSE 0
#define ERROR_VAL -1

// semaphore properties
#define SEM_FALG IPC_CREAT | 0666 // flag for creating the semaphore
#define SEM_INIT_VAL 1 // initial value of semaphore
#define SEM_LOCK -1 // indicates locking operation on semaphore
#define SEM_UNLOCK 1 // indicates unlocking operation on semaphore

union semun {
	int val;
	struct semid_ds *buf;
	ushort *array;
};

// struct to hold client's socket-fd and its nickname and its connection status
typedef struct {
	int isConnected; // indicates if client has connected
	int isRegistered; // indicates if client has given nickname
	int socketFd;
	char nickname[MSG_MAX_SIZE];
} ClientData;

// struct encapsulating all parameters to function handling a client on a new thread
typedef struct {

	ClientData* clients; // pointer to client's data
	int currClientSocket; // fd of socket for current client
	int semID; // semaphore id

} ThreadParams;

void initClientsArray(ClientData clients[MAX_CLIENTS]);
int getSemaphore();
int setupConnection(unsigned short port);
void* handleClient(void *arguments);
void rejectClient(int clientSockFd);
int getNickname(ClientData clients[MAX_CLIENTS], char nickname[MSG_MAX_SIZE],
		int nicknameMaxSize, int newClientIndex, int semID);
size_t readClientMessage(char msgBuffer[MSG_MAX_SIZE], int clientSockFd,
		int numBytesToRead, ClientData clients[MAX_CLIENTS], int clientIndex);
int isNicknameExists(ClientData clients[MAX_CLIENTS],
		char nickname[MAX_CLIENTS]);
int getNextClientIndex(ClientData clients[MAX_CLIENTS]);
void sendClientUserList(ClientData clients[MAX_CLIENTS], int recipeintIndex,
		int semID);
int isCloseMessage(char message[MSG_MAX_SIZE]);
void writeMessage(char message[MSG_MAX_SIZE], ClientData clients[MAX_CLIENTS],
		int sendingClientIndex, int semID);
void formatPublicMsg(char srcMessage[MSG_MAX_SIZE],
		char sendingClientName[MSG_MAX_SIZE], char formattedMsg[MSG_MAX_SIZE]);
void formatPrivateMsg(char srcMessage[MSG_MAX_SIZE],
		char sendingClientName[MSG_MAX_SIZE], char formattedMsg[MSG_MAX_SIZE]);
void extractRecipientName(char srcMessage[MSG_MAX_SIZE],
		char recipientNickname[MSG_MAX_SIZE]);
int findMsgContentStart(char message[MSG_MAX_SIZE]);
int getClientIndexByNickname(ClientData clients[MAX_CLIENTS],
		char nickname[MSG_MAX_SIZE]);
void writePublicMsg(char message[MSG_MAX_SIZE], ClientData clients[MAX_CLIENTS],
		int sendingClientIndex, int semID);
void writePrivateMsg(char message[MSG_MAX_SIZE],
		ClientData clients[MAX_CLIENTS], int recipientIndex, int semID);
void clearClientData(ClientData clients[MAX_CLIENTS], int clientIndex);

/**
 * Gets port number as parameter.
 * Binds a socket to given port and waits for
 * clients to connect.
 * Handles each connected client in a separate thread
 */
int main(int argc, char *argv[]) {

	int socketFd; // fd of socket to listen to
	unsigned short port; // port to get the data from
	pthread_t newThreadId; // identifier for new thread

	struct sockaddr_in clientAddrres; // holds client's address
	int dataStructLength = sizeof(struct sockaddr_in); // length of structure holding client's data

	ClientData clients[MAX_CLIENTS]; // array to hold data of each connected client
	int semID; // id of semaphore

	// validate number of arguments
	if (ARGS_NUM != argc) {

		// display error message and exit program
		printf("Wrong number of arguments!\n");
		exit(1);
	}

	// initialize array that holds clients data
	initClientsArray(clients);

	// create semaphore with MAX_CLIENTS sub-semaphores
	semID = getSemaphore();

	// extract port from arguments
	port = atoi(argv[1]);

	// create socket binded to port
	socketFd = setupConnection(port);

	// wait for clients to connect, handle each client in a new thread
	while (TRUE) {

		// initialize struct holding parameters for client handling
		ThreadParams parameters;

		// get socket fd of currently connecting client
		int connSocketFd = accept(socketFd, (struct sockaddr*) &clientAddrres,
				(socklen_t*) &dataStructLength);

		// check error
		if (-1 == connSocketFd) {

			// display error message and exit program
			perror("accept()");
			exit(1);
		}

		// set parameters
		parameters.clients = clients;
		parameters.currClientSocket = connSocketFd;
		parameters.semID = semID;

		// handle client in a new thread
		if (pthread_create(&newThreadId, NULL, handleClient, &parameters)) {

			// display error message and exit program
			perror("pthread_create()");
			exit(1);
		}
	}

	return EXIT_SUCCESS;
}

/**
 * Initializes clients array by setting at each entry
 * a client-data struct holding default values.
 * Default values are:
 * FALSE for connection and registration indicators, -1 as socket-fd
 * and empty string as nickname
 */
void initClientsArray(ClientData clients[MAX_CLIENTS]) {

	// set default value at each entry
	int i;
	for (i = 0 ; i < MAX_CLIENTS ; ++i) {

		// set client data holding default values
		ClientData defaultData;
		defaultData.isConnected = FALSE;
		defaultData.isRegistered = FALSE;
		defaultData.socketFd = -1;
		strcpy(defaultData.nickname, "");

		// add client data to clients array
		clients[i] = defaultData;
	}
}

/**
 * Creates a semaphore with MAX_CLIENTS sub-semaphores,
 * and sets initial value of each semaphore to be SEM_INIT_VAL.
 * If succeeds, returns semaphore id, otherwise, prints error message
 * and exits program
 */
int getSemaphore() {

	int i; // loop counter
	int semID; // id of semaphore
	ushort semInitVals[MAX_CLIENTS]; // array holding initial value for each sub-semaphore in set
	union semun initArgument; // argument for semaphore initialization

	// create semaphore with MAX_CLIENTS sub-semaphores
	if ((semID = semget(IPC_PRIVATE, MAX_CLIENTS, SEM_FALG)) == -1) {

		// display error message and exit program
		perror("semget()");
		exit(1);
	}

	initArgument.val = SEM_INIT_VAL; // set semaphore initial value

    // set initial value for each sub-semaphore
    for (i = 0 ; i < MAX_CLIENTS ; ++i) {

    	semInitVals[i] = SEM_INIT_VAL;
    }

    // attach the array to the initialization argument
    initArgument.array = semInitVals;

    // initialize semaphores
	if (semctl (semID , 0, SETALL , initArgument) == -1) {

		// display error message and exit program
		perror("semctl()");
		exit(1);
	}

	// return semaphore id
	return semID;
}

/**
 * Creates an IPv4 socket using TCP and binds it
 * to given port.
 * If succeeds, returns fd of the socket that was created,
 * otherwise, prints error message and exists program
 */
int setupConnection(unsigned short port) {

	struct sockaddr_in serverAddrres; // holds server's address
	int socketFd; // holds id of socket to be created

	// create IPv4 TCP socket
	if ((socketFd = socket(AF_INET, SOCK_STREAM, SOCKET_PROTOCOL)) < 0) {

		// display error message and exit program
		perror("socket()");
		exit(1);
	}

	// initialize server address using given port
	serverAddrres.sin_family = AF_INET;
	serverAddrres.sin_addr.s_addr = htonl(INADDR_ANY);
	serverAddrres.sin_port = htons(port);

	// bind socket to given port
	if (bind(socketFd, (struct sockaddr*) &serverAddrres,
			(socklen_t) sizeof(struct sockaddr_in)) == -1) {

		// display error message and exit program
		perror("bind()");
		exit(1);
	}

	// listen to socket
	if (listen(socketFd, MAX_CLIENTS) == -1) {

		// display error message and exit program
		perror("listen()");
		exit(1);
	}

	// return fd of created socket
	return socketFd;
}

/**
 * Runs in a separate thread, and handles client's input
 * and output messages.
 * Parameters are sent to the function in a struct holding:
 * 1. array of connected clients
 * 2. fd of client's socket
 * 3. id of semaphore to synchronize writing to clients' sockets
 * After client has chosen a nickname, sends message to all other clients that the new
 * client has connected.
 * Reads all of client's messages and sends them to the suitable recipient(s).
 * When client sends message to end connection, stops listening and closes socket.
 * If reading or writing to socket has errors, sets client's entry in clients array as
 * free and ends thread
 */
void* handleClient(void *arguments) {

	// extract parameters struct
	ThreadParams* parameters = (ThreadParams*)arguments;

	// get array of clients data
	ClientData* clients = parameters->clients;

	// extract client's socket fd
	int clientSockFd = parameters->currClientSocket;

	// extract semaphore id
	int semID = parameters->semID;

	// initialize buffer to hold messages from client
	char message[MSG_MAX_SIZE];

	// initialize buffer to hold client's nickname
	char nickname[MSG_MAX_SIZE];

	// indicates if should keep listening to client
	int keepListening = TRUE;

	// get new clinet's entry on connected clients array
	int clientIndex = getNextClientIndex(clients);

	// check if exceeds connections limit
	if (ERROR_VAL == clientIndex) {

		// reject client and end thread
		rejectClient(clientSockFd);
		return NULL;
	}

	// indicate that client has connected
	clients[clientIndex].isConnected = TRUE;

	// set client's socket
	clients[clientIndex].socketFd = clientSockFd;

	// get client's nickname
	if (!getNickname(clients, nickname, sizeof(nickname),clientIndex, semID)) {

		// indicate that connection is closed
		clearClientData(clients, clientIndex);

		// remote side close, end connection
		if (close(clientSockFd) == -1) {

			// display error message (on server) and exit thread
			perror("close()");
			pthread_exit(NULL);
		}

		// no return value
		return NULL;
	}

	// now client is registered
	clients[clientIndex].isRegistered = TRUE;

	// set client's nickname
	strcpy(clients[clientIndex].nickname, nickname);

	// notify all clients about the new client
	sprintf(message, NEW_USER_CONNECTED_MSG, nickname);
	writePublicMsg(message, clients, clientIndex, semID);

	// send client a list of all users in chat
	sendClientUserList(clients, clientIndex, semID);

	// keep listening to client until client ends connection
	while (keepListening) {

		// read client's message
		size_t bytesRead = readClientMessage(message, clientSockFd,
				sizeof(message) - 1, clients, clientIndex);

		// check remote side close
		if (0 == bytesRead) {

			// indicate that connection is closed
			clearClientData(clients, clientIndex);

			// end connection
			break;
		}

		// check if client sent end-connection message
		if (isCloseMessage(message)) {

			// indicate that connection is closed
			clearClientData(clients, clientIndex);

			// send client end-connection message
			writePrivateMsg(CLOSE_CONNECTION_MSG, clients, clientIndex, semID);

			// stop listening to client
			keepListening = FALSE;

			// notify all clients about the new client
			sprintf(message, "%s left the chat", nickname);
			writePublicMsg(message, clients, clientIndex, semID);

			break;
		}

		// send client's message to the correct recipient(s)
		writeMessage(message, clients, clientIndex, semID);

	}

	// done listening, close socket
	if (close(clientSockFd) == -1) {

		// display error message (on server) and exit thread
		perror("close()");
		pthread_exit(NULL);
	}

	// no return value
	return NULL;
}

/**
 * Sends reject message to client through given socket,
 * and closes the connection to this socket.
 * Function does not use writePrivateMsg since a rejected client
 * does not have a matching semaphore to lock the socket
 */
void rejectClient(int clientSockFd) {

	// compose reject message
	char message[MSG_MAX_SIZE];
	sprintf(message, REJECT_MSG, MAX_CLIENTS);

	// send reject message
	if (write(clientSockFd, message, strlen(message)) < strlen(message)) {

		// display error message (on server) and exit thread
		perror("write()");
		pthread_exit(NULL);
	}

	// close socket
	if (close(clientSockFd) == -1) {

		// display error message (on server) and exit thread
		perror("close()");
		pthread_exit(NULL);
	}
}

/**
 * Sends client welcome message and asks for client's nickname.
 * If nickname is already taken by another connected client,
 * asks for another name.
 * When done, sets nickname to given buffer and ends function.
 * If could read nickname successfully, returns TRUE. If detected
 * a remote side close, returns FALSE.
 * If had errors sending message or reading nickname, prints
 * (on server) error message and ends thread
 */
int getNickname(ClientData clients[MAX_CLIENTS], char nickname[MSG_MAX_SIZE],
		int nicknameMaxSize, int newClientIndex, int semID) {

	// flag to indicate if received a new nickname
	int isNicknameTaken = TRUE;

	// initialize welcome message
	char message[MSG_MAX_SIZE];
	strcpy(message, GET_NICKNAME_MSG);

	// send client welcome message
	writePrivateMsg(message, clients, newClientIndex, semID);

	// initialize message in case client sends a taken nickname
	strcpy(message, NICKNAME_TAKEN_MSG);

	// keep asking for a nickname until a new nickname is entered
	while (isNicknameTaken) {

		// read nickname
		size_t bytesRead = readClientMessage(nickname,
				clients[newClientIndex].socketFd, nicknameMaxSize, clients,
				newClientIndex);

		// check remote side close
		if (0 == bytesRead) {

			// error reading nickname
			return FALSE;
		}

		// check if nickname is taken by another connected client
		if (isNicknameExists(clients, nickname)) {

			// ask for another nickname
			writePrivateMsg(message, clients, newClientIndex, semID);

		} else {

			// got a new nickname
			isNicknameTaken = FALSE;
		}
	}

	// read nickname successfully
	return TRUE;
}

/**
 * Reads message from given client's socket, and writes message
 * to given buffer (reads number of bytes that is indicated in parameter).
 * Returns number of bytes read from client.
 * If had errors reading message, displays error message (on server),
 * sets client's entry in clients array as free and end thread
 */
size_t readClientMessage(char msgBuffer[MSG_MAX_SIZE], int clientSockFd,
		int numBytesToRead, ClientData clients[MAX_CLIENTS], int clientIndex) {

	size_t bytesRead; // counts how many bytes read from client

	// read client's message
	if ((bytesRead = read(clientSockFd, msgBuffer, numBytesToRead)) == -1) {

		// indicate that connection is closed
		clearClientData(clients, clientIndex);

		// display error message and exit program
		perror("close()");
		pthread_exit(NULL);
	}

	// end message string
	msgBuffer[bytesRead] = 0;

	// return number of bytes read
	return bytesRead;
}

/**
 * Gets clients array and checks if there is a connected
 * client who owns given nickname.
 * If there is one, returns TRUE, otherwise, FALSE
 */
int isNicknameExists(ClientData clients[MAX_CLIENTS],
		char nickname[MAX_CLIENTS]) {

	// check if another connected client holds given nickname
	int i;
	for (i = 0 ; i < MAX_CLIENTS ; ++i) {

		// if current client is registered, check nickname
		if (!clients[i].isRegistered) { continue; }
		if (strcmp(nickname, clients[i].nickname) == 0) {

			// found match, given nickname exists in clients arrays
			return TRUE;
		}
	}

	// no match found, nickname does not exist in clients array
	return FALSE;
}

/**
 * Gets clients array and find index of first client-data element
 * that is not connected.
 * If such entry is found, returns its index in the array.
 * Otherwise, returns ERROR_VAL
 */
int getNextClientIndex(ClientData clients[MAX_CLIENTS]) {

	// find first entry that its connection indicator is FALSE
	int i;
	for (i = 0 ; i < MAX_CLIENTS ; ++i) {

		// check current client data
		if (!clients[i].isConnected) {

			// current client is not connected, use this entry for new client
			return i;
		}
	}

	// could not find a free entry
	return ERROR_VAL;
}

/**
 * Sends to client matching recipient-index a message with the names
 * of all user's that are registered
 */
void sendClientUserList(ClientData clients[MAX_CLIENTS], int recipeintIndex,
		int semID) {

	int i; // loop counter

	// initialize message
	char message[MSG_MAX_SIZE];
	strcpy(message, DECLARE_REGISTERED_CLIENTS_MSG);

	// append all nicknames of clients
	for (i = 0 ; i < MAX_CLIENTS ; ++i) {

		// skip non-registered clients (they have no nickname yet)
		if (!clients[i].isRegistered) { continue; }

		// skip recipient client
		if (i == recipeintIndex) { continue; }

		// append current client's nickname
		strcat(message, " ");
		strcat(message, clients[i].nickname);
	}

	// send message
	writePrivateMsg(message, clients, recipeintIndex, semID);
}

/**
 * Checks if given message indicates that client wishes
 * to end the connection.
 * If so, returns TRUE, otherwise FALSE
 */
int isCloseMessage(char message[MSG_MAX_SIZE]) {

	// compare messages and return result
	return strcmp(message, CLOSE_CONNECTION_MSG) == 0;
}

/**
 * Parses given message and decides whether message should send to all clients,
 * or if should be a private message.
 * Composes the message in the correct format according to message type (public or private),
 * and sends the message accordingly.
 * If recipient nickname does not match any connected client or if message does not match
 * public or private message format, sends client an error message
 */
void writeMessage(char message[MSG_MAX_SIZE], ClientData clients[MAX_CLIENTS],
		int sendingClientIndex, int semID) {

	// initialize buffer to hold message after formatting given message according to its type
	char formattedMsg[MSG_MAX_SIZE];

	// check if public message (starts with "All:")
	char* firstMatch = strstr(message, "All:");
	if ((firstMatch != NULL) && (strcmp(message, firstMatch) == 0)) {

		// format message
		formatPublicMsg(message, clients[sendingClientIndex].nickname, formattedMsg);

		// write the formated message to all clients
		writePublicMsg(formattedMsg, clients, sendingClientIndex, semID);

		return;
	}

	// check if private message (starts with "To:")
	firstMatch = strstr(message, "To:");
	if ((firstMatch != NULL) && (strcmp(message, firstMatch) == 0)) {

		char recipientNickname[MSG_MAX_SIZE]; // holds recipient's nickname
		int recipientsIndex; // recipient's index in clients array

		// format message
		formatPrivateMsg(message, clients[sendingClientIndex].nickname, formattedMsg);

		// extract recipient nickname
		extractRecipientName(message, recipientNickname);

		// get recipient's index in clients array
		recipientsIndex = getClientIndexByNickname(clients, recipientNickname);

		// check if there is a registered client matching given nickname
		if (ERROR_VAL == recipientsIndex) {

			// no user matches nickname, send client error message
			writePrivateMsg(NO_RECIPIENT_MSG, clients, sendingClientIndex, semID);
			return;
		}

		// check if client tries to send message to him/herself
		if (sendingClientIndex == recipientsIndex) {

			// cannot send message, notify client
			writePrivateMsg(SELF_RECIPIENT_MSG, clients, sendingClientIndex, semID);
			return;
		}

		// write the formated message to specified client
		writePrivateMsg(formattedMsg, clients, recipientsIndex, semID);
		return;
	}

	// check if message's format is not correct
	if (!isCloseMessage(message)) {

		// notify client that message was of wrong format
		writePrivateMsg(ERROR_IN_FORMAT_MSG, clients, sendingClientIndex, semID);
	}
}

/**
 * Gets source message and nickname of client sending the message.
 * Saves in buffer for formatted message, a message in format:
 * "ClientNickName: Message"
 */
void formatPublicMsg(char srcMessage[MSG_MAX_SIZE],
		char sendingClientName[MSG_MAX_SIZE], char formattedMsg[MSG_MAX_SIZE]) {

	// compose message as a public message format
	strcpy(formattedMsg, sendingClientName); // write sending clinet's nickname
	strncat(formattedMsg, srcMessage + 3, strlen(srcMessage)-1); // concatenate message's content after "All"
}

/**
 * Gets source message and nickname of client sending the message.
 * Saves in buffer for formatted message, a message in format:
 * "SendingNickName_RecipientNickName: Message"
 */
void formatPrivateMsg(char srcMessage[MSG_MAX_SIZE],
		char sendingClientName[MSG_MAX_SIZE], char formattedMsg[MSG_MAX_SIZE]) {

	// compose message as a private message format
	strcpy(formattedMsg, sendingClientName); // write sending clinet's nickname
	strcat(formattedMsg, "_");
	strncat(formattedMsg, srcMessage + 3, strlen(srcMessage)-1); // concatenate message's content after "To:"
}

/**
 * Gets source message and extracts the recipient client nickname,
 * which is stored in given buffer
 */
void extractRecipientName(char srcMessage[MSG_MAX_SIZE],
		char recipientNickname[MSG_MAX_SIZE]) {

	// find index of message's content start (index of second ":")
	int contentStartIndex;
	contentStartIndex = findMsgContentStart(srcMessage);

	// copy message's part that is between "To:" and before content starts
	strncpy(recipientNickname, srcMessage + 3, contentStartIndex-3); // copy characters after "To:" until character before second ":"

	// terminate string
	recipientNickname[contentStartIndex-3] = 0;
}

/**
 * Gets message and returns index of one character before
 * content's start. For example, if the message is
 * "To:nickname: hello world", then function returns index
 * of second ":"
 */
int findMsgContentStart(char message[MSG_MAX_SIZE]) {

	int length = strlen(message); // get message's length
	int count = 0; // counts how many times found ":"
	int i; // loop counter

	// find second instance of ":"
	for (i = 0 ; i < length ; ++i) {

		// find match
		if (':' == message[i]) {

			// count match
			++count;
		}

		// check if found second match
		if (2 == count) { break; }
	}

	// return current index on message
	return i;
}

/**
 * Gets clients array and returns index of registered client matching
 * given nickname.
 * If no registered client matches given nickname, returns ERROR_VAL
 */
int getClientIndexByNickname(ClientData clients[MAX_CLIENTS],
		char nickname[MSG_MAX_SIZE]) {

	// find connected client matching given nickname
	int i;
	for (i = 0 ; i < MAX_CLIENTS ; ++i) {

		// skip non-registered clients
		if (!clients[i].isRegistered) { continue; }

		// compare current client's nickname
		if (strcmp(nickname, clients[i].nickname) == 0) {

			// match found, return client index
			return i;
		}
	}

	// no registered client with given nickname
	return ERROR_VAL;
}

/**
 * Writes given message to all clients except the sending client:
 * gets socket of each client using the data in clients array, and
 * skips client matching given client index.
 * Function uses semaphore to lock the socket of client that currently
 * writes the message to, and unlocks the socket when done writing.
 * If had error writing the message to at least one client, prints error
 * message (on server) and ends thread
 */
void writePublicMsg(char message[MSG_MAX_SIZE], ClientData clients[MAX_CLIENTS],
		int sendingClientIndex, int semID) {

	struct sembuf semBuf; // holds operation data for semaphore
	int i; // loop counter

	// send message to each client, except client who sends this message
	for (i = 0 ; i < MAX_CLIENTS ; ++i) {

		// skip sending client
		if (i == sendingClientIndex) { continue; }

		// skip non-registered clients
		if (!clients[i].isRegistered) { continue; }

		// lock current recipient's socket
		semBuf.sem_num = i;
		semBuf.sem_op = SEM_LOCK;
		semBuf.sem_flg = 0; // use no flags
		if (semop(semID, &semBuf, 1) == -1) {

			// indicate that connection is closed
			clearClientData(clients, sendingClientIndex);

			// display error message (on server) and exit thread
			perror("semop()");
			pthread_exit(NULL);
		}

		// write message to current client's socket
		if (write(clients[i].socketFd, message, strlen(message)) < strlen(message)) {

			// indicate that connection is closed
			clearClientData(clients, sendingClientIndex);

			// display error message (on server) and exit thread
			perror("write()");
			pthread_exit(NULL);
		}

		// unlock current recipient's socket
		semBuf.sem_num = i;
		semBuf.sem_op = SEM_UNLOCK;
		semBuf.sem_flg = 0; // use no flags
		if (semop(semID, &semBuf, 1) == -1) {

			// indicate that connection is closed
			clearClientData(clients, sendingClientIndex);

			// display error message (on server) and exit thread
			perror("semop()");
			pthread_exit(NULL);
		}
	}
}

/**
 * Writes given message to client matching given index in clients array:
 * gets socket of recipient using the data in clients array.
 * Function uses semaphore to lock the socket of recipient client, and
 * unlocks the socket when done writing.
 * If had error writing the message, prints error message (on server)
 * and ends thread
 */
void writePrivateMsg(char message[MSG_MAX_SIZE],
		ClientData clients[MAX_CLIENTS], int recipientIndex, int semID) {

	struct sembuf semBuf; // holds operation data for semaphore

	// lock recipient client's socket
	semBuf.sem_num = recipientIndex;
	semBuf.sem_op = SEM_LOCK;
	semBuf.sem_flg = 0; // use no flags
	if (semop(semID, &semBuf, 1) == -1) {

		// indicate that connection is closed
		clearClientData(clients, recipientIndex);

		// display error message (on server) and exit thread
		perror("semop()");
		pthread_exit(NULL);
	}

	// write message to recipient client's socket
	if (write(clients[recipientIndex].socketFd, message, strlen(message)) < strlen(message)) {

		// indicate that connection is closed
		clearClientData(clients, recipientIndex);

		// display error message (on server) and exit thread
		perror("write()");
		pthread_exit(NULL);
	}

	// unlock recipient client's socket
	semBuf.sem_num = recipientIndex;
	semBuf.sem_op = SEM_UNLOCK;
	semBuf.sem_flg = 0; // use no flags
	if (semop(semID, &semBuf, 1) == -1) {

		// indicate that connection is closed
		clearClientData(clients, recipientIndex);

		// display error message (on server) and exit thread
		perror("semop()");
		pthread_exit(NULL);
	}
}

void clearClientData(ClientData clients[MAX_CLIENTS], int clientIndex) {

	// set client entry at given index as not connected
	clients[clientIndex].isConnected = FALSE;
	clients[clientIndex].isRegistered = FALSE;
}
